/*
 * Copyright (c) 2020 - present, Inspur Genersoft Co., Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.iec.caf.data.jpa.repository.config;

import io.iec.caf.data.jpa.repository.CafI18nStringColumn;
import io.iec.caf.data.jpa.spi.RepositoryResourceLoaderResolver;
import io.iec.caf.data.jpa.utils.TemporaryHibernateIdentifier;
import io.iec.edp.caf.commons.runtime.CafEnvironment;
import io.iec.edp.caf.commons.runtime.env.enums.StartupMode;
import io.iec.edp.caf.data.multilang.CAFMultiLanguageColumn;
import lombok.extern.slf4j.Slf4j;
import lombok.var;
import io.iec.edp.caf.commons.thirdext.persister.entity.PersistersContext;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport;
import org.springframework.data.repository.config.RepositoryConfigurationDelegate;
import org.springframework.data.repository.config.RepositoryConfigurationExtension;
import org.springframework.data.repository.config.RepositoryConfigurationUtils;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Transient;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.*;

/**
 * This is {@link CafJpaRepositoriesRegistrar}.
 *
 * @author yisiqi
 * @since 1.0.0
 */
@Slf4j
public class CafJpaRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport implements BeanClassLoaderAware, BeanFactoryAware {

    private static final String BASE_PACKAGES = "basePackages";
    private static final String BASE_PACKAGE_CLASSES = "basePackageClasses";


    private ClassLoader classLoader;

    private ResourceLoader resourceLoader;

    private Environment environment;

    private final Map<String, List<String>> i18nColumnMap = new HashMap<>();

    private final Set<String> basePackages = new HashSet<>();

    private final Map<String, BeanDefinition> beanDefinitionMap = new HashMap<>();

    private BeanFactory beanFactory;

    private static List<RepositoryResourceLoaderResolver> repositoryResourceLoaderResolvers = new ArrayList<>();

    static {
        if(CafEnvironment.getStartupMode() != StartupMode.Serial){
            ServiceLoader<RepositoryResourceLoaderResolver> operations = ServiceLoader.load(RepositoryResourceLoaderResolver.class);
            Iterator<RepositoryResourceLoaderResolver> iterator = operations.iterator();
            while (iterator.hasNext()) {
                RepositoryResourceLoaderResolver operation = iterator.next();
//                try {
                    repositoryResourceLoaderResolvers.add(operation);
//                }  catch (Exception e) {
//                    throw new RuntimeException(e);
//                }
            }

            Assert.isTrue(repositoryResourceLoaderResolvers.size()!=0,"Parallel Start Mode,But can not get any [RepositoryResourceLoaderResolver],Please check");
        }

    }

    @NonNull
    @Override
    protected Class<? extends Annotation> getAnnotation() {
        return EnableCafJpaRepositories.class;
    }

    @NonNull
    @Override
    protected RepositoryConfigurationExtension getExtension() {
        return new CafJpaRepositoryConfigExtension();
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry, BeanNameGenerator generator) {
        //super.registerBeanDefinitions(metadata, registry, generator);
        Assert.notNull(metadata, "AnnotationMetadata must not be null!");
        Assert.notNull(registry, "BeanDefinitionRegistry must not be null!");
        Assert.notNull(resourceLoader, "ResourceLoader must not be null!");

        // Guard against calls for sub-classes
        if (metadata.getAnnotationAttributes(getAnnotation().getName()) == null) {
            return;
        }

        CafAnnotationRepositoryConfigurationSource configurationSource = new CafAnnotationRepositoryConfigurationSource(metadata,
                getAnnotation(), resourceLoader, environment, registry, generator);

        RepositoryConfigurationExtension extension = getExtension();
        RepositoryConfigurationUtils.exposeRegistration(extension, registry, configurationSource);

        RepositoryConfigurationDelegate delegate = new RepositoryConfigurationDelegate(configurationSource, resourceLoader,
                environment);

        delegate.registerRepositoriesIn(registry, extension);

        if (!TemporaryHibernateIdentifier.NATIVE) {
            Set<String> basePackages = getPackagesToScan(metadata);
            if (!basePackages.isEmpty()) {
                collectI18nBeanDefinition(basePackages);
            } else {
                log.warn("No entity scan base package found. If you have any JPA Entities, please make sure config them with \"@EnableCafJpaRepositories\" or \"@EntityScan\"");
            }
        }
    }

    private Set<String> getPackagesToScan(AnnotationMetadata metadata) {

        Set<String> packagesToScan = new LinkedHashSet<>();
        packagesToScan.addAll(getAnnotationBasePackages(metadata, EnableCafJpaRepositories.class));
        packagesToScan.addAll(getAnnotationBasePackages(metadata, EntityScan.class));

        return packagesToScan;
    }

    private Set<String> getAnnotationBasePackages(AnnotationMetadata metadata, Class<?> basePackagesAnnotation) {
        AnnotationAttributes attributes = AnnotationAttributes.fromMap(
                metadata.getAnnotationAttributes(basePackagesAnnotation.getCanonicalName()));
        if (attributes == null) {
            return Collections.emptySet();
        }
        String[] basePackages = Objects.requireNonNull(attributes).getStringArray(BASE_PACKAGES);
        String[] values = Objects.requireNonNull(attributes).getStringArray("value");
        Class<?>[] basePackageClasses = attributes.getClassArray(BASE_PACKAGE_CLASSES);
        Set<String> packagesToScan = new LinkedHashSet<>(Arrays.asList(basePackages));
        packagesToScan.addAll(Arrays.asList(values));
        for (Class<?> basePackageClass : basePackageClasses) {
            packagesToScan.add(ClassUtils.getPackageName(basePackageClass));
        }
        if (packagesToScan.isEmpty()) {
            return Collections.emptySet();
        }
        return packagesToScan;
    }

    private void collectI18nBeanDefinition(Set<String> scanBasePackages) {
        beanDefinitionMap.clear();
        basePackages.addAll(scanBasePackages);
        ClassPathScanningCandidateComponentProvider classScanner = getClassScanner();
        // collect all bean definitions in given base packages
        for (String basePack : basePackages) {
            Set<BeanDefinition> beanDefinitionSet = classScanner.findCandidateComponents(basePack);
            for (BeanDefinition beanDefinition : beanDefinitionSet) {
                beanDefinitionMap.put(beanDefinition.getBeanClassName(), beanDefinition);
            }
        }
        // collect column names according to the bean definitions
        for (Map.Entry<String, BeanDefinition> beanDefEntry : beanDefinitionMap.entrySet()) {
            BeanDefinition beanDefinition = beanDefEntry.getValue();
            if (!(beanDefinition instanceof AnnotatedBeanDefinition)) {
                continue;
            }
            Class<?> clazz = null;
            try {
                String className = beanDefinition.getBeanClassName();
                clazz = classLoader.loadClass(className);
            } catch (ClassNotFoundException e) {
                log.error(e.getMessage(),e);
            }
            assert clazz != null;
            resolveCafI18nBeanDefinition(clazz);
        }
        log.debug("Found CAF i18n entities columns mapping: {}.", i18nColumnMap);
        PersistersContext.appendI18nEntityColumnsMap(i18nColumnMap);
    }

    private void resolveEmbeddedClass(Class<?> embeddedClazz) {
        resolveCafI18nBeanDefinition(embeddedClazz);
    }

    private void resolveSuperClass(Class<?> superClazz) {
        resolveCafI18nBeanDefinition(superClazz);
    }

    private void resolveCafI18nBeanDefinition(Class<?> clazz) {
        Set<String> columnNames = new HashSet<>();
        Class<?> superClazz = clazz.getSuperclass();
        if (beanDefinitionMap.containsKey(superClazz.getCanonicalName())) {
            resolveSuperClass(superClazz);
            List<String> superClazzColumns = i18nColumnMap.get(superClazz.getCanonicalName());
            if (superClazzColumns != null) {
                columnNames.addAll(superClazzColumns);
            }
        }
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            if (field.getAnnotation(Transient.class) != null) {
                continue;
            }
            if (field.getAnnotation(Embedded.class) != null) {
                if (field.getType() == CAFMultiLanguageColumn.class) {
                    columnNames.add(field.getName());
                } else {
                    resolveEmbeddedClass(field.getType());
                    List<String> embeddedClazzColumns = i18nColumnMap.get(field.getType().getCanonicalName());
                    if (embeddedClazzColumns != null) {
                        columnNames.addAll(embeddedClazzColumns);
                    }
                }
            } else if (field.getAnnotation(CafI18nStringColumn.class) != null) {
                String columnName = Optional.ofNullable(field.getAnnotation(Column.class))
                        .map(Column::name)
                        .filter(name -> !"".equals(name)) // name in Column annotation is set to "" if user doesn't set explicitly
                        .orElseGet(field::getName);
                columnNames.add(columnName);
            }
        }
        if (!columnNames.isEmpty()) {
            i18nColumnMap.put(clazz.getCanonicalName(), new ArrayList<>(columnNames));
            log.debug("Found Entity \"{}\" contains CAF i18n columns: {}.", clazz.getCanonicalName(), columnNames);
        }
    }

    private ClassPathScanningCandidateComponentProvider getClassScanner() {
        ClassPathScanningCandidateComponentProvider classScanner = new ClassPathScanningCandidateComponentProvider(false, this.environment);
        classScanner.setResourceLoader(resourceLoader);
        classScanner.addIncludeFilter(new AssignableTypeFilter(Object.class));
        return classScanner;
    }

    @Override
    public void setBeanClassLoader(@NonNull ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    @Override
    public void setEnvironment(@NonNull Environment environment) {
        super.setEnvironment(environment);
        this.environment = environment;
    }

    @Override
    public void setResourceLoader(@NonNull ResourceLoader resourceLoader) {
        ResourceLoader rs = resourceLoader;
        if(repositoryResourceLoaderResolvers.size()>0){
            RepositoryResourceLoaderResolver resolver = repositoryResourceLoaderResolvers.get(0);
            if(resolver!=null){
                if(log.isDebugEnabled()){
                    log.debug("RepositoryResourceLoaderResolver Type ["+(resolver!=null?resolver.getClass().getName():null)+"]");
                }
                var temp = resolver.getResourceLoader(this.beanFactory);
                if(temp!=null){
                    rs = temp;
                }
            }
        }


//        if (this.moduleManager != null) {
//            rs = new HibernateResourceResolver(this.moduleManager, ModuleManager.class.getClassLoader());
//        }

        super.setResourceLoader(rs);
        this.resourceLoader = rs;
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
//        if (beanFactory instanceof PlatformBeanFactory) {
//            this.moduleManager = ((PlatformBeanFactory) beanFactory).getModuleManager();
//        } else if (beanFactory instanceof ModuleBeanFactory) {
//            this.moduleManager = ((PlatformBeanFactory) ((HierarchicalBeanFactory) beanFactory).getParentBeanFactory()).getModuleManager();
//        }
    }
}
